package org.fujay.langchain4jserver.application.service.impl;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.fujay.commons.core.util.JsonUtils;
import org.fujay.commons.langchain4j.rag.processor.result.DocumentProcessResult;
import org.fujay.langchain4jserver.application.assembler.KnowledgeBaseAssembler;
import org.fujay.langchain4jserver.application.command.CreateKnowledgeBaseCommand;
import org.fujay.langchain4jserver.application.command.UpdateKnowledgeBaseInfoCommand;
import org.fujay.langchain4jserver.application.dto.FileDTO;
import org.fujay.langchain4jserver.application.dto.KnowledgeBaseDTO;
import org.fujay.langchain4jserver.application.dto.KnowledgeBaseIndexStatusDTO;
import org.fujay.langchain4jserver.application.dto.KnowledgeBaseSummaryDTO;
import org.fujay.langchain4jserver.application.service.KnowledgeBaseService;
import org.fujay.langchain4jserver.domain.knowledge.aggregate.KnowledgeBaseAggregate;
import org.fujay.langchain4jserver.domain.knowledge.entity.File;
import org.fujay.langchain4jserver.domain.knowledge.event.FileAddedEvent;
import org.fujay.langchain4jserver.domain.knowledge.event.FileIndexingFailed;
import org.fujay.langchain4jserver.domain.knowledge.event.FileRemovedEvent;
import org.fujay.langchain4jserver.domain.knowledge.event.KnowledgeBaseDeletedEvent;
import org.fujay.langchain4jserver.domain.knowledge.exception.KnowledgeBaseNotFoundException;
import org.fujay.langchain4jserver.domain.knowledge.repository.FileRepository;
import org.fujay.langchain4jserver.domain.knowledge.repository.KnowledgeBaseRepository;
import org.fujay.langchain4jserver.domain.knowledge.service.IndexingService;
import org.fujay.langchain4jserver.domain.knowledge.vo.FileId;
import org.fujay.langchain4jserver.domain.knowledge.vo.IndexStatus;
import org.fujay.langchain4jserver.domain.knowledge.vo.KnowledgeBaseId;
import org.fujay.langchain4jserver.domain.knowledge.vo.StorageStrategy;
import org.fujay.langchain4jserver.domain.model.aggregate.Model;
import org.fujay.langchain4jserver.domain.model.repository.ModelRepository;
import org.fujay.langchain4jserver.domain.model.vo.ModelId;
import org.fujay.langchain4jserver.domain.model.vo.ModelType;
import org.fujay.langchain4jserver.domain.model.vo.TestResult;
import org.fujay.langchain4jserver.domain.shared.exception.FileStorageException;
import org.fujay.langchain4jserver.domain.shared.service.StorageProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * 知识库应用服务实现
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class KnowledgeBaseServiceImpl implements KnowledgeBaseService {

    private final KnowledgeBaseRepository knowledgeBaseRepository;
    private final FileRepository fileRepository;
    private final IndexingService indexingService;
    private final ModelRepository modelRepository;
    private final ThreadPoolTaskExecutor taskExecutor;
    private final StorageProvider storageProvider;
    private final ApplicationEventPublisher eventPublisher;

    @Value("${app.storage.default-strategy:LOCAL}")
    private String defaultStorageStrategyValue;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public KnowledgeBaseDTO createKnowledgeBase(CreateKnowledgeBaseCommand command) {
        // 验证 Embedding Model 是否存在且类型正确
        ModelId embeddingModelId = command.getEmbeddingModelId();
        validateEmbeddingModel(embeddingModelId);

        KnowledgeBaseId id = KnowledgeBaseId.create();
        String indexName = generateIndexName(id);
        String namespace = ObjectUtil.defaultIfBlank(command.getNameSpace(), "default");
        KnowledgeBaseAggregate aggregate =
                KnowledgeBaseAggregate.create(
                        id,
                        command.getName(),
                        command.getDescription(),
                        indexName,
                        embeddingModelId,
                        namespace);
        knowledgeBaseRepository.save(aggregate);
        log.info("创建新知识库成功: id={}, 名称={}, 索引名={}", id.value(), command.getName(), indexName);

        KnowledgeBaseIndexStatusDTO initialStatus = getKnowledgeBaseStatusInternal(id);
        return KnowledgeBaseAssembler.toDTO(aggregate, initialStatus);
    }

    @Override
    public Optional<KnowledgeBaseDTO> getKnowledgeBase(KnowledgeBaseId id) {
        return knowledgeBaseRepository
                .findById(id)
                .map(
                        aggregate -> {
                            KnowledgeBaseIndexStatusDTO status = getKnowledgeBaseStatusInternal(id);
                            return KnowledgeBaseAssembler.toDTO(aggregate, status);
                        });
    }

    @Override
    public List<KnowledgeBaseSummaryDTO> listKnowledgeBases() {
        List<KnowledgeBaseAggregate> aggregates = knowledgeBaseRepository.findAllShallow();
        return KnowledgeBaseAssembler.toSummaryDTOList(aggregates);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateKnowledgeBaseInfo(UpdateKnowledgeBaseInfoCommand command) {
        KnowledgeBaseId id = command.getId();
        KnowledgeBaseAggregate aggregate =
                knowledgeBaseRepository
                        .findById(id)
                        .orElseThrow(() -> new KnowledgeBaseNotFoundException(id));

        ModelId modelId = aggregate.getEmbeddingModelId();
        if (ObjectUtil.isNotNull(command.getModelId())) {
            ModelId embeddingModelId = command.getModelId();
            validateEmbeddingModel(embeddingModelId);
            modelId = command.getModelId();
        }
        aggregate.updateInfo(command.getName(), command.getDescription(), modelId);
        knowledgeBaseRepository.save(aggregate);
        log.info("更新知识库信息成功: {}", command.getId().value());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteKnowledgeBase(KnowledgeBaseId id) {
        KnowledgeBaseAggregate aggregate = knowledgeBaseRepository.findByIdShallow(id).orElseThrow(() -> new KnowledgeBaseNotFoundException(id));
        String indexName = aggregate.getKnowledgeBase().getIndexName();

        knowledgeBaseRepository.deleteById(id);
        log.info("已从数据库删除知识库记录: {}", id.value());

        eventPublisher.publishEvent(new KnowledgeBaseDeletedEvent(this, id, indexName));
        log.info("已发布知识库删除事件: id={}, indexName={}", id.value(), indexName);
    }

    // --- 文件管理 ---

    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileDTO uploadAndAddFile(KnowledgeBaseId knowledgeBaseId, MultipartFile file) {
        log.info("接收到文件上传，知识库ID: {}, 文件名: {}", knowledgeBaseId.value(), file.getOriginalFilename());

        if (file.isEmpty()) {
            throw new IllegalArgumentException("不能添加空文件。");
        }

        KnowledgeBaseAggregate aggregate =
                knowledgeBaseRepository
                        .findById(knowledgeBaseId)
                        .orElseThrow(() -> new KnowledgeBaseNotFoundException(knowledgeBaseId));

        String originalFilename =
                StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
        String fileExtension = FilenameUtils.getExtension(originalFilename);
        String baseName = FilenameUtils.getBaseName(originalFilename);
        String uniqueFileName = IdUtil.fastSimpleUUID() + "-" + baseName + "." + fileExtension;
        String relativePath =
                Paths.get(String.valueOf(knowledgeBaseId.value()), uniqueFileName).toString();
        StorageStrategy strategy = StorageStrategy.valueOf(defaultStorageStrategyValue.toUpperCase());

        log.debug("准备存储文件 '{}' 到路径 '{}'，策略: {}", originalFilename, relativePath, strategy);

        String storedPath;
        try {
            storedPath = storageProvider.store(file.getInputStream(), relativePath, strategy);
            log.info("文件成功存储到: {}", storedPath);
        } catch (Exception e) {
            log.error("存储文件 {} 到知识库 {} 失败", relativePath, knowledgeBaseId.value(), e);
            throw new FileStorageException("上传文件存储失败: " + e.getMessage(), e);
        }

        FileId fileId = FileId.create();
        File newFile = File.create(
                fileId,
                knowledgeBaseId,
                originalFilename,
                storedPath,
                file.getSize(),
                file.getContentType(),
                strategy);

        aggregate.addFile(newFile);
        knowledgeBaseRepository.save(aggregate);

        eventPublisher.publishEvent(new FileAddedEvent(this, fileId, originalFilename, storedPath));
        log.info(
                "已添加文件记录: fileId={}, fileName={} 到知识库: {}",
                fileId.value(),
                originalFilename,
                knowledgeBaseId.value());
        return KnowledgeBaseAssembler.toDTO(newFile);
    }

    @Override
    public List<FileDTO> listFiles(KnowledgeBaseId knowledgeBaseId) {
        List<File> files = fileRepository.findByKnowledgeBaseId(knowledgeBaseId);
        return KnowledgeBaseAssembler.toFileDTOList(files);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteFile(KnowledgeBaseId knowledgeBaseId, FileId fileId) {
        log.info("尝试删除文件 {} 从知识库 {}", fileId.value(), knowledgeBaseId.value());
        KnowledgeBaseAggregate aggregate =
                knowledgeBaseRepository
                        .findById(knowledgeBaseId)
                        .orElseThrow(() -> new KnowledgeBaseNotFoundException(knowledgeBaseId));

        File fileToRemove = aggregate.getFiles().get(fileId);
        if (fileToRemove == null) {
            log.warn("文件 {} 不在知识库 {} 中，无法删除。", fileId.value(), knowledgeBaseId.value());
            return;
        }

        aggregate.removeFile(fileId);
        knowledgeBaseRepository.save(aggregate);

        try {
            storageProvider.deleteFile(fileToRemove);
            log.info("已删除物理文件: {}", fileToRemove.getFilePath());
        } catch (Exception e) {
            log.error(
                    "尝试删除物理文件 {} 失败 (FileId: {}, KBId: {}). Error: {}",
                    fileToRemove.getFilePath(),
                    fileId.value(),
                    knowledgeBaseId.value(),
                    e.getMessage(),
                    e);
        }
        eventPublisher.publishEvent(new FileRemovedEvent(this, knowledgeBaseId, fileId));
        log.info("文件 {} 已从知识库 {} 删除", fileId.value(), knowledgeBaseId.value());
    }

    // --- 索引触发 (异步) ---
    private void triggerIndexFileAsyncInternal(KnowledgeBaseId knowledgeBaseId, FileId fileId) {
        taskExecutor.execute(
                () -> {
                    try {
                        log.info("开始异步索引文件: kbId={}, fileId={}", knowledgeBaseId.value(), fileId.value());
                        DocumentProcessResult result = indexingService.indexFile(fileId);
                        knowledgeBaseRepository
                                .findById(knowledgeBaseId)
                                .ifPresent(
                                        knowledgeBase -> {
                                            if (result.getFailureCount() != 0) {
                                                knowledgeBase.updateFileIndexStatus(
                                                        fileId, null, IndexStatus.FAILED, "文件索引失败");
                                                log.error(JsonUtils.toJsonString(result.getErrors()));
                                            } else {
                                                knowledgeBase.updateFileIndexStatus(
                                                        fileId, result.getDocumentIds(), IndexStatus.COMPLETED, null);
                                            }
                                        });

                    } catch (Exception e) {
                        log.error(
                                "异步索引文件失败: kbId={}, fileId={}, error={}",
                                knowledgeBaseId.value(),
                                fileId.value(),
                                e.getMessage(),
                                e);
                        eventPublisher.publishEvent(
                                new FileIndexingFailed(this, knowledgeBaseId, fileId, e.getMessage()));
                    }
                });
    }

    @Override
    public void triggerIndexFileAsync(KnowledgeBaseId knowledgeBaseId, FileId fileId) {
        CompletableFuture.runAsync(
                () -> triggerIndexFileAsyncInternal(knowledgeBaseId, fileId), taskExecutor);
    }

    private void triggerIndexKnowledgeBaseAsyncInternal(KnowledgeBaseId knowledgeBaseId) {
        taskExecutor.execute(
                () -> {
                    try {
                        log.info("开始异步索引整个知识库: kbId={}", knowledgeBaseId.value());
                        indexingService.indexKnowledgeBase(knowledgeBaseId);
                    } catch (Exception e) {
                        log.error("异步索引知识库失败: kbId={}, error={}", knowledgeBaseId.value(), e.getMessage(), e);
                    }
                });
    }

    @Override
    public void triggerIndexKnowledgeBaseAsync(KnowledgeBaseId knowledgeBaseId) {
        CompletableFuture.runAsync(
                () -> triggerIndexKnowledgeBaseAsyncInternal(knowledgeBaseId), taskExecutor);
    }

    private void triggerRebuildIndexAsyncInternal(KnowledgeBaseId knowledgeBaseId) {
        taskExecutor.execute(
                () -> {
                    try {
                        log.info("开始异步重建知识库索引: kbId={}", knowledgeBaseId.value());
                        indexingService.rebuildIndex(knowledgeBaseId);
                    } catch (Exception e) {
                        log.error("异步重建知识库索引失败: kbId={}, error={}", knowledgeBaseId.value(), e.getMessage(), e);
                    }
                });
    }

    @Override
    public void triggerRebuildIndexAsync(KnowledgeBaseId knowledgeBaseId) {
        CompletableFuture.runAsync(
                () -> triggerRebuildIndexAsyncInternal(knowledgeBaseId), taskExecutor);
    }

    @Override
    public KnowledgeBaseIndexStatusDTO getKnowledgeBaseStatus(KnowledgeBaseId knowledgeBaseId) {
        return getKnowledgeBaseStatusInternal(knowledgeBaseId);
    }

    private KnowledgeBaseIndexStatusDTO getKnowledgeBaseStatusInternal(
            KnowledgeBaseId knowledgeBaseId) {
        log.debug("获取知识库状态: kbId={}", knowledgeBaseId.value());
        KnowledgeBaseAggregate aggregate =
                knowledgeBaseRepository
                        .findById(knowledgeBaseId)
                        .orElseThrow(() -> new KnowledgeBaseNotFoundException(knowledgeBaseId));

        List<File> files = aggregate.listFiles();
        int total = files.size();
        int pending = (int) countFilesByStatus(files, IndexStatus.PENDING);
        int processing =
                (int)
                        countFilesByStatus(
                                files, IndexStatus.PARSING, IndexStatus.VECTORIZING, IndexStatus.STORING);
        int completed = (int) countFilesByStatus(files, IndexStatus.COMPLETED);
        int failed = (int) countFilesByStatus(files, IndexStatus.FAILED);

        KnowledgeBaseIndexStatusDTO dto = new KnowledgeBaseIndexStatusDTO();
        dto.setKnowledgeBaseId(knowledgeBaseId.value());
        dto.setKnowledgeBaseStatus(
                aggregate.getStatus() != null ? aggregate.getStatus().name() : "UNKNOWN");
        dto.setTotalFiles(total);
        dto.setPendingFiles(pending);
        dto.setProcessingFiles(processing);
        dto.setCompletedFiles(completed);
        dto.setFailedFiles(failed);

        List<KnowledgeBaseIndexStatusDTO.FailedFileDetailDTO> failedDetails =
                files.stream()
                        .filter(f -> f.getIndexStatus() == IndexStatus.FAILED)
                        .limit(10)
                        .map(
                                f ->
                                        new KnowledgeBaseIndexStatusDTO.FailedFileDetailDTO(
                                                f.getId().value(), f.getFileName(), f.getErrorMessage()))
                        .collect(Collectors.toList());
        dto.setFailedFilesDetails(failedDetails);

        log.debug("知识库状态获取完成: kbId={}, status={}", knowledgeBaseId.value(), dto);
        return dto;
    }

    // --- 辅助方法 ---

    private String generateIndexName(KnowledgeBaseId id) {
        return "kb_" + id.value();
    }

    private long countFilesByStatus(List<File> files, IndexStatus... statuses) {
        if (CollectionUtils.isEmpty(files) || statuses == null || statuses.length == 0) {
            return 0;
        }
        List<IndexStatus> targetStatuses = List.of(statuses);
        return files.stream().filter(f -> targetStatuses.contains(f.getIndexStatus())).count();
    }

    private void validateEmbeddingModel(ModelId embeddingModelId) {
        if (embeddingModelId == null) {
            throw new IllegalArgumentException("必须提供嵌入模型ID");
        }
        Optional<Model> modelOpt = modelRepository.findById(embeddingModelId);
        if (modelOpt.isEmpty()) {
            throw new IllegalArgumentException("指定的嵌入模型ID不存在: " + embeddingModelId.value());
        }
        Model model = modelOpt.get();
        if (model.getModelType() != ModelType.EMBEDDING) {
            throw new IllegalArgumentException("指定的模型ID不是嵌入模型类型: " + embeddingModelId.value());
        }

        if (!model.isEnabled()) {
            throw new IllegalArgumentException("指定的模型未启用: " + embeddingModelId.value());
        }

        if (model.getConnectionStatus().lastTestResult() != TestResult.SUCCESS) {
            throw new IllegalArgumentException("模型连通性测试未通过: " + embeddingModelId.value());
        }
        log.debug("嵌入模型验证通过: {}", embeddingModelId.value());
    }
}
